feat(executor): opt-in per-block retry for transient failures - #6298
feat(executor): opt-in per-block retry for transient failures#6298waleedlatif1 wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview
Reviewed by Cursor Bugbot for commit 693340f. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 693340f. Configure here.
| data?: BlockData | ||
| layout?: BlockLayoutState | ||
| locked?: boolean | ||
| retry?: BlockRetryConfig |
There was a problem hiding this comment.
API schema strips retry config
High Severity
retry was added to BlockState and the serializer, but workflowBlockStateSchema still omits it. Zod strips unknown keys on PUT/GET /api/workflows/[id]/state and import, so the opt-in setting cannot persist through the API path this PR relies on until UI exists.
Reviewed by Cursor Bugbot for commit 693340f. Configure here.
| if (candidate.code && RETRYABLE_ERROR_CODES.has(candidate.code)) return true | ||
| if (typeof candidate.status === 'number' && RETRYABLE_HTTP_STATUSES.has(candidate.status)) { | ||
| return true | ||
| } |
There was a problem hiding this comment.
HTTP status classification misses errors
High Severity
isRetryableBlockError only reads status, but generic tool failures attach Sim-owned codes as statusCode and leave upstream statuses on output. Most integration blocks therefore never retry on 408/429/502/503/504 despite opting in.
Reviewed by Cursor Bugbot for commit 693340f. Configure here.
| delayMs, | ||
| error: normalizeError(error), | ||
| }) | ||
| await sleep(delayMs) |
There was a problem hiding this comment.
Cancel ignored after backoff sleep
Medium Severity
Abort is checked only before sleep, and sleep is not abort-aware. A run cancelled during backoff still starts another handler attempt afterward, contrary to the mid-backoff cancellation guarantee.
Reviewed by Cursor Bugbot for commit 693340f. Configure here.
Greptile SummaryAdds opt-in transient-error retries around block-handler invocation, including retry policy types, serialization, error classification, attempt logging, and executor tests.
Confidence Score: 4/5The cancellation and persistence failures should be fixed before merging because retries can run after cancellation and configured policies can silently disappear. The retry loop does not recheck cancellation after its timer, while the newly supported retry field is omitted from deserialization and normalized workflow persistence. Files Needing Attention: apps/sim/executor/execution/block-executor.ts, apps/sim/serializer/index.ts, and the normalized workflow persistence mappings
|
| Filename | Overview |
|---|---|
| apps/sim/executor/execution/block-executor.ts | Adds the retry loop around handler invocation, but cancellation during backoff can still permit another attempt. |
| apps/sim/executor/execution/block-retry.ts | Implements bounded policy resolution and structural transient-error classification with explicit ineligible cases. |
| apps/sim/serializer/index.ts | Emits retry configuration during serialization, but the reverse and persistence round trips do not preserve it. |
| packages/workflow-types/src/workflow.ts | Defines the shared retry policy and bounds, while exposing a field that is not yet carried through normalized persistence. |
| apps/sim/executor/execution/block-executor.retry.test.ts | Covers retry behavior and synchronous cancellation, but not cancellation arriving during backoff. |
| apps/sim/executor/execution/block-retry.test.ts | Thoroughly covers policy bounds, ineligible block types, transient classification, aborts, and cyclic causes. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Invoke block handler] -->|Success| B[Post-process output]
A -->|Failure| C{Retry enabled and transient?}
C -->|No| D[Error port or throw]
C -->|Yes| E[Backoff sleep]
E --> F{Execution cancelled?}
F -->|Yes| G[Stop retrying]
F -->|No| A
Reviews (1): Last reviewed commit: "feat(executor): opt-in per-block retry f..." | Re-trigger Greptile
| delayMs, | ||
| error: normalizeError(error), | ||
| }) | ||
| await sleep(delayMs) |
There was a problem hiding this comment.
Cancellation bypasses retry backoff
When an execution is cancelled during the backoff sleep, the loop starts the next handler attempt without rechecking the signal, causing a side-effecting block to run again after the execution was stopped.
| await sleep(delayMs) | |
| await sleep(delayMs) | |
| if (ctx.abortSignal?.aborted === true) throw error |
Knowledge Base Used: Workflow Executor
| @@ -327,6 +327,7 @@ export class Serializer { | |||
| color: blockConfig.bgColor, | |||
There was a problem hiding this comment.
When a retry-enabled workflow is deserialized or saved and reloaded through normalized persistence, the reverse serializer and persistence mappings omit retry, causing the configured policy to disappear and later executions to invoke the block only once.
Knowledge Base Used: Workspace Frontend (Workflow Editor UI)
…, read statusCode


Summary
retrysetting (maxAttempts,waitMs) that replays the block handler when a failure looks transient. Off by default.block-executor.ts, so every block type is covered without touching any block definition — the same seam the error port uses.retryOnFailis opt-in per node) rather than Temporal's (retry-by-default), because Temporal can assume idempotent activities and we can't: replaying "post message" or "create ticket" after an ambiguous transport failure duplicates a real side effect. The builder decides.Design notes
TimeoutError, socket-levelcodes, and transient HTTP statuses (408/429/502/503/504). A 4xx other than 408/429 is refused because it'll be rejected identically on replay. The one message-based check is Bun's dropped-connection string, which carries nocode— isolated to a single named constant.ProviderErroroverwritesname, so the classification survives only oncause. Bounded against cyclic causes.blockLog.attemptsis set only when a block actually retried.Type of Change
Testing
21 new tests across
block-retry.test.ts(eligibility + classification) andblock-executor.retry.test.ts(loop behaviour, ceiling, cancellation, error-port composition). Every guard was verified fail-detectable by breaking it and watching the test go red — that process caught one vacuous test of my own (a bareAbortErroris unretryable anyway, so the guard needed an abort wrapping a retryable cause to be exercised).Executor + serializer suites: 1612 passing. The one failure in
executor/handlers/pi/cloud-review-tools.test.tsis pre-existing — verified by stashing this branch and reproducing it on cleanorigin/staging. Typecheck, lint, andcheck:api-validationclean.Follow-up
No UI yet — this is the executor/serializer plumbing, so the setting is only reachable via the API/serialized workflow. A shared control alongside the existing block-level toggles is the natural next step.
Checklist